OS: Windows 10
Editor: Visual Studio Code
Rust version: 1.63.0
閉包,如今在許多程式語言都有實作這項功能,可以把函式實作的部分(匿名函式)賦職給其他變數,或是當成參數傳入其他函式內。
或許會說,C的函式指標(function pointer)不就可以做到這些事嗎?是沒錯,但在作用域(scope)的規範下,閉包能做到的還是跟函式指標有區別的,下面則會說到這部分,先來看看如何在Rust中使用閉包。
首先,這是一個簡單的函式:
fn add_one(x: u32) -> u32 {
x + 1
}
然後以下是,把上面的函式變為閉包的樣子,可以看到有不同的寫法,可以發現定義閉包,都是以||
開始,裡面放的都會是要傳入參數:
fn main() {
let add_one_v1 = |x: u32| -> u32 { x + 1 };
let add_one_v2 = |x| { x + 1 };
let add_one_v3 = |x| x + 1;
}
如果把上面的方法複製拿來用的話,會發生錯誤,因為compiler不知道參數跟回傳值是什麼型別。add_one_v2
跟add_one_v3
會需要在使用後,由compiler推導他的型別才會生效。
要使用閉包的話,跟使用函式的方式一樣:
add_one_v1(3);
add_one_v2(4);
add_one_v3(5);
PS: add_one_v2
可能會被rust analyzer簡化成add_one_v3
的樣子,因為實作只有入參數加1,然後回傳忠家沒有任何陳述句(statement)。
如果沒有要傳入任何參數的話,會是長這樣,以我們要一個回傳2的函式為例:
let two = || 2; // `two`的type是`|| -> i32`
println!("{}!", two());
再來請注意這樣的閉包定義方式,使用的時候,會是由compiler推導使用型別,假設連續呼叫著閉包,但傳入的都是不同型別,compiler會丟出錯誤,因為在第一次使用的時候,這個閉包已經被判斷為第一次使用的型別了:
let return_val = |x| x;
let s = return_val(String::from("Example"));
let p = return_val(2); //ERROR!
Compiling basic v0.1.0 (/Users/liangcharlie/Workspace/rust-learning/basic)
error[E0308]: mismatched types
--> src/main.rs:20:24
|
20 | let p = return_val(2);
| ^- help: try using a conversion method: `.to_string()`
| |
| expected struct `String`, found integer
For more information about this error, try `rustc --explain E0308`.
coompiler會說閉包使用的時候,推倒的時候已經是String
型別,但第二次呼叫的時候是i32
。這是不允許的。
對於閉包,我們也可以存在結構內:
use std::collections::HashMap;
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
table: HashMap<u32, u32>,
}
可以看到泛型的結構內,我們對T
進行限制,限制閉包要傳的參數型別與回傳型別,Fn
是一個內建的特徵,除了Fn
,還有FnMut
跟FnOnce
。
然後下面是實作與簡單的測試:
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
table: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32 {
if let Some(&value) = self.table.get(&arg) {
value
} else {
let nv = (self.calculation)(arg);
self.table.insert(arg, nv);
nv
}
}
}
fn main() {
let mut c = Cacher::new(|a| a + 6);
let v1 = c.value(1);
println!("{}", v1);
let v2 = c.value(2);
println!("{}", v2);
}
但這裡先不展開FnMut
跟FnOnce
的用法,僅介紹他們的區別,等之後遇到再來補充他們。
FnOnce
: 只能執行一次,取用的數值所有權會被轉移(move)Fn
: 取用的值為不可變的借用FnMut
: 取用的值為可變的借用以上的說明可以對比到正常使用函式時,對參數所有權的處理。
最上面有提到函式指標(以C
為例)與閉包是有差別,差別在於,你可以使用與閉包定義在同個環境的變數,例如這樣:
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
if equal_to_x(y) {
println!("Is same.");
} else {
println!("Not the same.");
}
}
但在函式是不行的,會被compiler抱怨,說並不在同個環境內:
// 錯誤!不可執行
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool {
z == x // `x`在作用域外
}
let y = 4;
if equal_to_x(y) {
println!("Is same.");
} else {
println!("Not the same.");
}
}